"""
回测性能分析和可视化模块
"""
from typing import Dict, List, Optional, Tuple
import os
import pandas as pd
import numpy as np
import plotly.graph_objs as go
from plotly.subplots import make_subplots
from engine.object import BacktestResultSchema
from pandera.typing import DataFrame
import json
from datetime import datetime
from string import Template
from loguru import logger

class PerformanceAnalyzer:
    """回测性能分析器"""
    
    def __init__(self, results: DataFrame[BacktestResultSchema]):
        """
        初始化性能分析器
        
        Args:
            results: 回测结果DataFrame，包含以下列：
                - date: 日期
                - total_value: 总资产
                - returns: 收益率
        """
        self.results = results.copy()
        
        # 检查是否已经有date作为索引或列
        if 'date' in results.columns:
            self.results['date'] = pd.to_datetime(self.results['date'])
            self.results.set_index('date', inplace=True)
        else:
            # 如果date不是列，可能已经是索引
            self.results.index = pd.to_datetime(self.results.index)
        
        # 初始化指标字典
        self.metrics = {}
        
        # 计算性能指标
        self._calculate_metrics()
    
    def _calculate_metrics(self) -> None:
        """计算性能指标"""
        # 计算累积收益率
        self.results['cumulative_returns'] = (1 + self.results['returns']).cumprod() - 1
        
        # 计算回撤
        rolling_max = self.results['total_value'].expanding().max()
        drawdown = (self.results['total_value'] - rolling_max) / rolling_max
        self.results['drawdown'] = drawdown
        
        # 计算换手率和平均持仓标的数量
        turnover_rate = 0.0
        avg_holdings_count = 0.0
        if 'weights' in self.results.columns:
            # 计算换手率 - 手动计算字典之间的差异，而不是使用pandas的diff()
            daily_turnover = []
            prev_weights = None
            for idx, weights_dict in enumerate(self.results['weights']):
                if isinstance(weights_dict, dict):
                    if prev_weights is not None:
                        # 计算当前权重和前一个权重的差异
                        all_symbols = set(weights_dict.keys()) | set(prev_weights.keys())
                        turnover_sum = 0.0
                        for symbol in all_symbols:
                            current_weight = weights_dict.get(symbol, 0.0)
                            prev_weight = prev_weights.get(symbol, 0.0)
                            turnover_sum += abs(current_weight - prev_weight)
                        daily_turnover.append(turnover_sum)
                    prev_weights = weights_dict
            
            if daily_turnover:
                # 计算平均每日换手率，并年化
                turnover_rate = np.mean(daily_turnover) * 252
            
            # 计算平均持仓标的数量
            daily_holdings_count = []
            for weights_dict in self.results['weights']:
                if isinstance(weights_dict, dict):
                    # 计算权重大于0的标的数量
                    holdings_count = sum(1 for value in weights_dict.values() if value > 0)
                    daily_holdings_count.append(holdings_count)
            
            if daily_holdings_count:
                avg_holdings_count = np.mean(daily_holdings_count)
        
        # 计算主要指标
        self.metrics.update({
            'total_return': self.results['cumulative_returns'].iloc[-1],
            'annualized_return': self._calculate_annualized_return(),
            'max_drawdown': self.results['drawdown'].min(),
            'sharpe_ratio': self._calculate_sharpe_ratio(),
            'win_rate': len(self.results[self.results['returns'] > 0]) / len(self.results),
            'volatility': self.results['returns'].std() * np.sqrt(252),
            'sortino_ratio': self._calculate_sortino_ratio(),
            'turnover_rate': turnover_rate,  
            'avg_holdings_count': avg_holdings_count,  
            'duration':self.results['duration'].mean()
        })
        
        # 计算卡玛比率
        self.metrics['calmar_ratio'] = self._calculate_calmar_ratio()
    
    def _calculate_annualized_return(self) -> float:
        """
        计算年化收益率
        使用几何平均计算，考虑复利效应
        """
        # 获取起始和结束日期
        start_date = self.results.index[0]
        end_date = self.results.index[-1]
        
        # 计算精确的年数（考虑闰年）
        days = (end_date - start_date).days
        years = days / 365.25  # 使用365.25考虑闰年
        
        # 计算总收益率
        start_value = self.results['total_value'].iloc[0]
        end_value = self.results['total_value'].iloc[-1]
        total_return = end_value / start_value - 1
        
        # 使用几何平均计算年化收益率
        return (1 + total_return) ** (1 / years) - 1 if years > 0 else 0
    
    def _calculate_sharpe_ratio(self, risk_free_rate: float = 0.02) -> float:
        """
        计算夏普比率
        
        Args:
            risk_free_rate: 无风险利率，默认2%
        """
        returns = self.results['returns']
        excess_returns = returns - risk_free_rate / 252  # 日化无风险利率
        return np.sqrt(252) * excess_returns.mean() / returns.std() if returns.std() != 0 else 0
    
    def _calculate_sortino_ratio(self, risk_free_rate: float = 0.02) -> float:
        """
        计算索提诺比率
        
        Args:
            risk_free_rate: 无风险利率，默认2%
        """
        returns = self.results['returns']
        excess_returns = returns - risk_free_rate / 252
        downside_returns = returns[returns < 0]
        downside_std = np.sqrt(np.mean(downside_returns**2)) if len(downside_returns) > 0 else 0
        return np.sqrt(252) * excess_returns.mean() / downside_std if downside_std != 0 else 0
    
    def _calculate_calmar_ratio(self) -> float:
        """计算卡玛比率"""
        max_drawdown = abs(self.metrics['max_drawdown'])
        return self.metrics['annualized_return'] / max_drawdown if max_drawdown != 0 else 0
    
    def get_metrics(self) -> Dict[str, float]:
        """获取性能指标"""
        return self.metrics

class PerformanceVisualizer:
    """回测性能可视化器"""
    
    def __init__(self, analyzer: PerformanceAnalyzer, trades: Optional[List] = None):
        """
        初始化可视化器
        
        Args:
            analyzer: 性能分析器实例
            trades: 交易记录列表（可选）
        """
        self.analyzer = analyzer
        self.results = analyzer.results
        self.metrics = analyzer.metrics
        self.trades_detail = self.results.trades
        self.trades = trades or []
        
        # 获取模板路径
        self.template_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'templates',
            'report_template.html'
        )
    
    def plot_performance(self) -> Tuple[go.Figure, go.Figure]:
        """
        绘制性能图表和持仓占比图
        
        Returns:
            Tuple[go.Figure, go.Figure]: 性能图表和持仓占比图
        """
        # 创建性能图表
        perf_fig = make_subplots(
            rows=4, cols=1,
            subplot_titles=('净值曲线与交易点', None, None, '收益率分布'),  
            vertical_spacing=0.05,  
            specs=[[{"secondary_y": True}],
                  [{}],  
                  [{}],  
                  [{"type": "histogram"}]],  
            shared_xaxes=False  # 不共享X轴，以便为每个图表单独设置时间刻度
        )
        
        # 净值曲线
        perf_fig.add_trace(
            go.Scatter(
                x=self.results.index,
                y=self.results['total_value'],
                name='总资产',
                line=dict(color='#1f77b4')
            ),
            row=1, col=1
        )
        
        # 添加买卖点标注
        if self.trades:
            buy_dates = []
            buy_values = []
            buy_texts = []
            sell_dates = []
            sell_values = []
            sell_texts = []
            
            for trade in self.trades:
                trade_date = trade.timestamp
                if trade_date in self.results.index:
                    trade_value = self.results.loc[trade_date, 'total_value']
                    trade_text = f"价格: {trade.price:.2f}<br>数量: {trade.quantity}"
                    
                    if trade.direction > 0:  # 买入点
                        buy_dates.append(trade_date)
                        buy_values.append(trade_value)
                        buy_texts.append(trade_text)
                    else:  # 卖出点
                        sell_dates.append(trade_date)
                        sell_values.append(trade_value)
                        sell_texts.append(trade_text)
            
            # 添加买入点
            if buy_dates:
                perf_fig.add_trace(
                    go.Scatter(
                        x=buy_dates,
                        y=buy_values,
                        mode='markers+text',
                        name='买入',
                        text=buy_texts,
                        textposition="top center",
                        hovertemplate="%{text}<extra></extra>",
                        marker=dict(
                            symbol='triangle-up',
                            size=10,
                            color='red'
                        )
                    ),
                    row=1, col=1
                )
            
            # 添加卖出点
            if sell_dates:
                perf_fig.add_trace(
                    go.Scatter(
                        x=sell_dates,
                        y=sell_values,
                        mode='markers+text',
                        name='卖出',
                        text=sell_texts,
                        textposition="bottom center",
                        hovertemplate="%{text}<extra></extra>",
                        marker=dict(
                            symbol='triangle-down',
                            size=10,
                            color='green'
                        )
                    ),
                    row=1, col=1
                )
        
        # 回撤
        perf_fig.add_trace(
            go.Scatter(
                x=self.results.index,
                y=self.results['drawdown'],
                name='回撤',
                fill='tozeroy',
                line=dict(color='#d62728')
            ),
            row=2, col=1
        )
        
        # 组合久期曲线
        perf_fig.add_trace(
            go.Scatter(
                x=self.results.index,
                y=self.results['duration'],
                name='组合久期',
                line=dict(color='#9467bd')
            ),
            row=3, col=1
        )
        
        # 收益率分布
        perf_fig.add_trace(
            go.Histogram(
                x=self.results['returns'],
                name='收益率分布',
                nbinsx=50,
                marker_color='#2ca02c'
            ),
            row=4, col=1
        )
        
        # 更新布局
        perf_fig.update_layout(
            height=1100,  # 增加高度以适应新增的久期曲线图
            title=dict(
                text='回测性能分析',
                x=0.5,
                xanchor='center'
            ),
            showlegend=True,
            legend=dict(
                orientation="h",
                yanchor="bottom",
                y=1.02,
                xanchor="right",
                x=1
            ),
            hovermode='x unified'
        )
        
        # 更新x轴设置，显示详细的时间刻度
        date_range = self.results.index.to_series()
        date_diff = (date_range.max() - date_range.min()).days
        
        # 根据回测时间长度动态确定最合适的时间间隔
        if date_diff <= 30:  # 少于一个月，每天显示
            tick_interval = 'D1'
            date_format = '%m-%d'
        elif date_diff <= 90:  # 少于三个月，每周显示
            tick_interval = 'W1'
            date_format = '%m-%d'
        elif date_diff <= 365:  # 少于一年，每月显示
            tick_interval = 'M1'
            date_format = '%Y-%m'
        else:  # 超过一年，每季度显示
            tick_interval = 'M3'
            date_format = '%Y-%m'
        
        # 为净值曲线设置刻度
        perf_fig.update_xaxes(
            title_text=None,
            tickformat=date_format,
            tickangle=-45,
            dtick=tick_interval,
            ticklabelmode="period",
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=1, col=1
        )
        
        # 移除回撤图的X轴刻度
        perf_fig.update_xaxes(
            showticklabels=False,  # 不显示刻度标签
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=2, col=1
        )
        
        # 移除久期图的X轴刻度
        perf_fig.update_xaxes(
            showticklabels=False,  # 不显示刻度标签
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=3, col=1
        )
        
        # 收益率分布的X轴格式
        perf_fig.update_xaxes(
            title_text="收益率",
            tickformat=".2%",  # 百分比格式
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=4, col=1
        )
        
        # 更新Y轴网格线和标题
        perf_fig.update_yaxes(
            title_text="总资产",
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=1, col=1
        )
        
        perf_fig.update_yaxes(
            title_text="回撤",
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            tickformat=".2%",  # 百分比格式
            row=2, col=1
        )
        
        perf_fig.update_yaxes(
            title_text="久期(年)",
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=3, col=1
        )
        
        perf_fig.update_yaxes(
            title_text="频率",
            showgrid=True,
            gridwidth=1,
            gridcolor='lightgray',
            row=4, col=1
        )
        
        # 创建持仓占比面积图
        pos_fig = go.Figure()
        
        if hasattr(self.analyzer.results, 'weights'):
            positions_df = pd.DataFrame(self.analyzer.results.weights.to_dict()).T
            
            # 计算持仓占比
            total_value = positions_df.sum(axis=1)
            position_weights = positions_df.div(total_value, axis=0) * 100
            
            # 过滤掉所有日期持仓均为0的标的
            non_zero_columns = []
            for column in position_weights.columns:
                # 使用更严格的过滤标准，只保留最大持仓超过0.1%的标的
                if position_weights[column].max() > 0.1:
                    non_zero_columns.append(column)
            
            # 只保留有显著持仓的标的
            position_weights = position_weights[non_zero_columns]
            
            # 添加颜色设置
            colors = [
                'rgba(31, 119, 180, 0.6)',  # 蓝色
                'rgba(255, 127, 14, 0.6)',  # 橙色
                'rgba(44, 160, 44, 0.6)',   # 绿色
                'rgba(214, 39, 40, 0.6)',   # 红色
                'rgba(148, 103, 189, 0.6)', # 紫色
                'rgba(140, 86, 75, 0.6)',   # 棕色
                'rgba(227, 119, 194, 0.6)', # 粉色
                'rgba(127, 127, 127, 0.6)', # 灰色
                'rgba(188, 189, 34, 0.6)',  # 黄绿色
                'rgba(23, 190, 207, 0.6)'   # 青色
            ]
            
            # 添加每个资产的面积图
            for i, column in enumerate(position_weights.columns):
                color = colors[i % len(colors)]
                pos_fig.add_trace(
                    go.Scatter(
                        x=position_weights.index,
                        y=position_weights[column],
                        name=column,
                        mode='lines',
                        stackgroup='one',
                        line=dict(width=0.5),
                        fillcolor=color,
                        hovertemplate='%{y:.1f}%<extra></extra>'
                    )
            )
        
        # 更新持仓图布局
        pos_fig.update_layout(
            title=dict(
                text='持仓占比变化',
                x=0.5,
                xanchor='center',
                font=dict(size=16)
            ),
            xaxis_title='日期',
            yaxis_title='持仓占比',
            height=400,
            showlegend=True,
            hovermode='x unified',
            yaxis=dict(
                tickformat='.1%',
                range=[0, 100]
            ),
            template='plotly_white',
            legend=dict(
                orientation='h',
                yanchor='bottom',
                y=1.02,
                xanchor='center',
                x=0.5
            )
        )
        
        return perf_fig, pos_fig
        
    def export_html_report(self, filename: str) -> None:
        """
        生成HTML回测报告
        
        Args:
            filename: 输出的HTML文件名
        """
        # 创建图表
        perf_fig, pos_fig = self.plot_performance()
        
        # 读取HTML模板
        with open(self.template_path, 'r', encoding='utf-8') as f:
            template = Template(f.read())
            
        # 读取Plotly库文件内容，用于内联到HTML中
        plotly_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            'static',
            'js',
            'plotly.min.js'
        )
        plotly_content = ""
        try:
            with open(plotly_path, 'r', encoding='utf-8') as f:
                plotly_content = f.read()
            logger.info(f"成功读取Plotly库文件: {plotly_path}")
        except Exception as e:
            logger.error(f"无法读取Plotly库文件，将使用CDN版本: {str(e)}")
            plotly_content = "" # 如果读取失败，留空使用CDN备用方案
        
        # 将图表转换为JSON
        perf_json = json.loads(perf_fig.to_json())
        pos_json = json.loads(pos_fig.to_json())
        
        # 处理交易数据
        trades_json = []
        for trades in self.trades_detail:
            
            if trades :
                for trade in trades:
                    if trade.quantity == 0:
                        continue
                    # 转换TradeData对象为字典
                    trade_dict = {
                        'timestamp': trade.timestamp.isoformat(),
                        'symbol': trade.symbol,
                        'direction': 1 if trade.direction > 0 else -1,
                        'price': trade.price,
                        'quantity': trade.quantity,
                        'commission': trade.commission,
                        'transaction_cost': trade.commission + getattr(trade, 'slippage', 0)
                    }
                    trades_json.append(trade_dict)
        
        # 处理持仓详情数据
        positions_detail_json = {}
        
        # 处理现金流数据
        cashflows_json = []
        
        # 检查是否存在现金流数据
        for date, row in self.results.iterrows():
            date_str = date.strftime('%Y-%m-%d')

            if 'cashflows' in row and row['cashflows']:
                for cashflow in row['cashflows']:
                    # 转换CashFlowData对象为字典
                    cashflow_dict = {
                        'timestamp': cashflow.timestamp.isoformat(),
                        'symbol': cashflow.symbol,
                        'type': cashflow.cashflow_type.value,
                        'amount': cashflow.amount,
                        'related_position': cashflow.related_position or '',
                        'is_outflow': cashflow.is_outflow()
                    }
                    cashflows_json.append(cashflow_dict)
        
        # 检查结果中的所有列
        logger.info(f"结果数据框的列: {list(self.results.columns)}")
        
        # 检查是否存在position_detail字段
        if 'position_detail' in self.results.columns:
            logger.info("找到 position_detail 列")
            
            # 处理持仓详情，使用Position对象的完整信息
            for date, row in self.results.iterrows():
                date_str = date.strftime('%Y-%m-%d')

                # 初始化该日期的持仓列表
                positions_detail_json[date_str] = []
                
                # 添加该日期的现金数据
                if 'cash' in row:
                    positions_detail_json[date_str + '_cash'] = float(row['cash'])
                
                # position_detail是列表
                if isinstance(row['position_detail'], list):
                    positions_list = row['position_detail']
                    for position in positions_list:
                        if position.quantity <= 0:
                            continue
                            
                        # 提取Position对象的所有需要展示的属性
                        position_dict = {
                            'symbol': position.symbol,
                            'quantity': position.quantity,
                            'price': position.current_price,
                            'avg_price': position.avg_price,
                            'market_value': position.market_value(),
                            'clean_value': position.quantity * position.current_price,
                            'accrued_interest': position.calculate_accrued_interest(position.current_date),
                            'holding_period': position.holding_period,
                            'duration': position.duration,  # 添加久期字段
                            'asset_type': position.asset_type.value if hasattr(position.asset_type, 'value') else str(position.asset_type)
                        }
                        
                        # 添加到该日期的持仓列表
                        positions_detail_json[date_str].append(position_dict)
                else:
                    logger.error(f"日期 {date_str} 的 position_detail 不是列表: {type(row['position_detail'])}")
        
        # 记录最终的持仓数据，便于调试
        if positions_detail_json:
            sample_date = list(positions_detail_json.keys())[0]
            logger.info(f"持仓详情数据示例 ({sample_date}): {positions_detail_json[sample_date][:2]}")
        
        # 准备模板变量
        template_vars = {
            'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            'start_date': self.results.index[0].strftime("%Y-%m-%d"),
            'end_date': self.results.index[-1].strftime("%Y-%m-%d"),
            'total_return': f"{self.metrics['total_return']:.2%}",
            'annualized_return': f"{self.metrics['annualized_return']:.2%}",
            'max_drawdown': f"{self.metrics['max_drawdown']:.2%}",
            'sharpe_ratio': f"{self.metrics['sharpe_ratio']:.2f}",
            'sortino_ratio': f"{self.metrics['sortino_ratio']:.2f}",
            'calmar_ratio': f"{self.metrics['calmar_ratio']:.2f}",
            'volatility': f"{self.metrics['volatility']:.2%}",
            'win_rate': f"{self.metrics['win_rate']:.2%}",
            'turnover_rate': f"{self.metrics['turnover_rate']:.2%}",
            'avg_holdings_count': f"{self.metrics['avg_holdings_count']:.1f}",
            'perf_plotly_json': json.dumps(perf_json),
            'pos_plotly_json': json.dumps(pos_json),
            'trades_json': json.dumps(trades_json),
            'positions_json': json.dumps(positions_detail_json),
            'cashflows_json': json.dumps(cashflows_json),
            'plotly_js': plotly_content
        }
        
        # 生成HTML报告
        html_content = template.safe_substitute(template_vars)
        
        # 保存报告
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(html_content)


def create_parameter_surface_html(param_results, x_param, y_param, z_param=None, color_param=None, title="参数优化3D表面图", filename="parameter_surface.html", z_title=None):
    """
    根据参数优化结果创建3D参数表面图并输出为独立HTML文件
    支持在界面上自由切换所有可用的指标
    
    Args:
        param_results (list): 参数优化结果列表，每个元素为包含参数和指标的字典
        x_param (str): x轴参数名称
        y_param (str): y轴参数名称
        z_param (str, optional): 默认的z轴指标名称，如果不提供则使用第一个可用的指标
        title (str, optional): 图表标题
        filename (str, optional): 输出的HTML文件名
        z_title (str, optional): z轴标题，如果不提供则使用z_param
        
    Returns:
        str: 生成的HTML文件路径
    """
    import pandas as pd
    import numpy as np
    import plotly.graph_objects as go
    from datetime import datetime
    import os
    import json
    
    # 转换为DataFrame便于处理
    df = pd.DataFrame(param_results)
    
    # 识别所有可用指标（非参数列且为数值型）
    all_columns = list(df.columns)
    param_columns = [x_param, y_param]
    
    # 只保留数值型列作为可选指标
    metric_columns = []
    for col in all_columns:
        if col not in param_columns:
            # 检查列的数据类型是否为数值型
            if np.issubdtype(df[col].dtype, np.number):
                metric_columns.append(col)
            else:
                logger.warning(f"列 '{col}' 不是数值型，将被排除在Z轴选项之外")
    
    # 如果没有指定z_param，使用第一个可用的指标
    if not z_param or z_param not in metric_columns:
        if metric_columns:
            z_param = metric_columns[0]
        else:
            raise ValueError("没有可用的指标列用于Z轴")
            
    # 如果没有指定color_param，默认使用avg_holdings_count，如果不存在则使用z_param
    if not color_param:
        if 'avg_holdings_count' in metric_columns:
            color_param = 'avg_holdings_count'
        else:
            color_param = z_param
    
    # 检查参数是否存在
    required_params = [x_param, y_param, z_param]
    missing_params = [param for param in required_params if param not in df.columns]
    if missing_params:
        raise ValueError(f"参数缺失: {', '.join(missing_params)}")
        
    # 获取x和y参数的所有唯一值
    x_values = sorted(df[x_param].unique())
    y_values = sorted(df[y_param].unique())
    
    # 创建网格数据
    x_grid, y_grid = np.meshgrid(x_values, y_values)
    z_grid = np.zeros_like(x_grid, dtype=float)
    
    # 填充z值
    for i, y_val in enumerate(y_values):
        for j, x_val in enumerate(x_values):
            # 找到对应的结果行
            result = df[(df[x_param] == x_val) & (df[y_param] == y_val)]
            if not result.empty:
                z_grid[i, j] = result[z_param].values[0]
            else:
                z_grid[i, j] = np.nan  # 如果没有对应的值，设为NaN
    
    # 获取颜色数据
    color_grid = np.zeros_like(x_grid, dtype=float)
    for i, y_val in enumerate(y_values):
        for j, x_val in enumerate(x_values):
            # 找到对应的结果行
            result = df[(df[x_param] == x_val) & (df[y_param] == y_val)]
            if not result.empty:
                color_grid[i, j] = result[color_param].values[0]
            else:
                color_grid[i, j] = np.nan  # 如果没有对应的值，设为NaN
                
    # 创建3D表面图
    fig = go.Figure(data=[go.Surface(
        z=z_grid,
        x=x_values,
        y=y_values,
        surfacecolor=color_grid,
        colorscale='Viridis',
        colorbar=dict(title=color_param),
        name=z_param
    )])
    
    # 设置z轴标题
    if z_title is None:
        z_title = z_param
    
    # 设置图表布局
    fig.update_layout(
        title={
            'text': title,
            'y':0.95,
            'x':0.5,
            'xanchor': 'center',
            'yanchor': 'top'
        },
        scene={
            'xaxis_title': x_param,
            'yaxis_title': y_param,
            'zaxis_title': z_title,
            'camera': {
                'eye': {'x': 1.5, 'y': 1.5, 'z': 1.2}
            }
        },
        margin=dict(l=0, r=0, b=0, t=50),
        template="plotly_white",
        autosize=True,
    )
    
    # 添加最优参数点
    if "min" in z_param.lower() or "drawdown" in z_param.lower() or "turnover" in z_param.lower() or "volatility" in z_param.lower():
        # 对于需要最小化的指标
        best_idx = df[z_param].idxmin()
        best_text = "最小值"
    else:
        # 对于需要最大化的指标
        best_idx = df[z_param].idxmax()
        best_text = "最大值"
    
    best_row = df.loc[best_idx]
    best_x = best_row[x_param]
    best_y = best_row[y_param]
    best_z = best_row[z_param]
    
    # 添加最优点标记
    fig.add_trace(go.Scatter3d(
        x=[best_x],
        y=[best_y],
        z=[best_z],
        mode='markers+text',
        marker=dict(color='red', size=8, symbol='circle'),
        text=[f"{best_text}: {best_z:.4f}"],
        textposition="top center",
        name=f"最优参数 ({best_x}, {best_y})"
    ))
    
    # 添加轮廓图（投影）
    fig.add_trace(go.Contour(
        z=color_grid,
        x=x_values,
        y=y_values,
        colorscale='Viridis',
        showscale=False,
        opacity=0.8,
        contours=dict(showlabels=True),
        visible=False,  # 初始隐藏
        name="轮廓图"
    ))
    
    # 添加热力图（投影）
    fig.add_trace(go.Heatmap(
        z=color_grid,
        x=x_values,
        y=y_values,
        colorscale='Viridis',
        showscale=False,
        visible=False,  # 初始隐藏
        name="热力图"
    ))
    
    # 添加参数表
    value_table = []
    for _, row in df.iterrows():
        value_table.append([
            row[x_param],
            row[y_param],
            row[z_param]
        ])
    
    fig.add_trace(go.Table(
        header=dict(
            values=[x_param, y_param, z_param],
            align='center',
            font=dict(size=12)
        ),
        cells=dict(
            values=list(map(list, zip(*value_table))),
            align='center'
        )
    ))
    
    # 创建字典来存储每个指标的网格数据
    surface_data = {}
    color_data = None
    best_points = {}
    
    for metric in metric_columns:
        z_grid = np.zeros_like(x_grid, dtype=float)
        
        # 填充z值
        for i, y_val in enumerate(y_values):
            for j, x_val in enumerate(x_values):
                # 找到对应的结果行
                result = df[(df[x_param] == x_val) & (df[y_param] == y_val)]
                if not result.empty:
                    z_grid[i, j] = result[metric].values[0]
                else:
                    z_grid[i, j] = np.nan  # 如果没有对应的值，设为NaN
                    
        # 保存网格数据
        surface_data[metric] = z_grid
        
        # 如果是颜色指标，保存颜色数据
        if metric == color_param:
            color_data = z_grid
        
        # 计算最优点（根据指标类型决定是最大还是最小）
        if "min" in metric.lower() or "drawdown" in metric.lower() or "turnover" in metric.lower() or "volatility" in metric.lower():
            # 对于需要最小化的指标
            best_idx = df[metric].idxmin()
            best_text = "最小值"
        else:
            # 对于需要最大化的指标
            best_idx = df[metric].idxmax()
            best_text = "最大值"
            
        best_row = df.loc[best_idx]
        best_points[metric] = {
            'x': best_row[x_param],
            'y': best_row[y_param],
            'z': best_row[metric],
            'text': best_text
        }
    
    # 编号系统生成（用于指标切换）
    # 初始情况下只显示默认z_param的图表元素
    visible_initial = []
    for m in range(len(metric_columns)):
        # 如果是默认指标，则可见，否则隐藏
        is_default = metric_columns[m] == z_param
        # 对3D表面图和最优点
        visible_initial.extend([is_default, is_default])
        # 对等高线图和热力图（隐藏）
        visible_initial.extend([False, False])
    
    # 再添加数据表（隐藏）
    visible_initial.append(False)
    
    # 创建图表对象
    fig = go.Figure()
    
    # 为每个指标添加3D表面图和最优点、等高线图和热力图
    for i, metric in enumerate(metric_columns):
        z_grid = surface_data[metric]
        best_point = best_points[metric]
        
        # 添加3D表面图
        fig.add_trace(go.Surface(
            z=z_grid,
            x=x_values,
            y=y_values,
            surfacecolor=surface_data[color_param] if color_param in surface_data else z_grid,
            colorscale='Viridis',
            colorbar=dict(title=color_param),
            name=metric,
            visible=True if metric == z_param else False
        ))
        
        # 添加最优点标记
        fig.add_trace(go.Scatter3d(
            x=[best_point['x']],
            y=[best_point['y']],
            z=[best_point['z']],
            mode='markers+text',
            marker=dict(color='red', size=8, symbol='circle'),
            text=[f"{best_point['text']}: {best_point['z']:.4f}"],
            textposition="top center",
            name=f"最优参数 ({best_point['x']}, {best_point['y']})",
            visible=True if metric == z_param else False
        ))
        
        # 添加等高线图（默认隐藏）
        fig.add_trace(go.Contour(
            z=surface_data[color_param] if color_param in surface_data else z_grid,
            x=x_values,
            y=y_values,
            colorscale='Viridis',
            showscale=False,
            opacity=0.8,
            contours=dict(showlabels=True),
            visible=False,
            name=f"{metric} 等高线图"
        ))
        
        # 添加热力图（默认隐藏）
        fig.add_trace(go.Heatmap(
            z=surface_data[color_param] if color_param in surface_data else z_grid,
            x=x_values,
            y=y_values,
            colorscale='Viridis',
            showscale=False,
            visible=False,
            name=f"{metric} 热力图"
        ))
    
    # 添加参数表（包含所有指标）
    headers = [x_param, y_param] + metric_columns
    cell_values = []
    for col in headers:
        cell_values.append(df[col].tolist())
        
    fig.add_trace(go.Table(
        header=dict(
            values=headers,
            align='center',
            font=dict(size=12)
        ),
        cells=dict(
            values=cell_values,
            align='center',
            font=dict(size=11)
        ),
        visible=False,
        name="参数表"
    ))
    
    # 准备指标切换按钮
    metric_buttons = []
    for i, metric in enumerate(metric_columns):
        # 条件为: 将这个指标的图表元素设为可视状态，其他所有元素设为非可视状态
        visible = [False] * len(visible_initial)
        # 设置当前指标的3D表面图和最优点为可视
        visible[i*4] = True   # 3D表面图
        visible[i*4+1] = True # 最优点
        
        button = dict(
            label=metric,
            method="update",
            args=[{"visible": visible},
                  {"scene": {
                      "zaxis": {"title": metric}
                  }}]
        )
        metric_buttons.append(button)
    
    # 准备颜色指标切换按钮
    color_buttons = []
    for i, metric in enumerate(metric_columns):
        # 创建用于更新所有表面图和热力图颜色的函数
        button = dict(
            label=metric,
            method="update",
            args=[{}, {}]  # 占位符，将在自定义函数中更新
        )
        color_buttons.append(button)
    
    # 准备视图切换按钮
    view_buttons = []
    
    # 3D表面图按钮
    view_buttons.append(dict(
        label="3D表面图",
        method="update",
        args=[{
            # 当前选中指标的3D表面图和最优点可视，其他都不可视
            "visible": [i//4 == metric_columns.index(z_param) and i%4 < 2 for i in range(len(metric_columns)*4)] + [False]
        }, {
            "scene": {"visible": True},
            "xaxis": {"visible": False},
            "yaxis": {"visible": False}
        }]
    ))
    
    # 等高线图按钮
    view_buttons.append(dict(
        label="等高线图",
        method="update",
        args=[{
            # 当前选中指标的等高线图可视，其他都不可视
            "visible": [i//4 == metric_columns.index(z_param) and i%4 == 2 for i in range(len(metric_columns)*4)] + [False]
        }, {
            "scene": {"visible": False},
            "xaxis": {"visible": True},
            "yaxis": {"visible": True}
        }]
    ))
    
    # 热力图按钮
    view_buttons.append(dict(
        label="热力图",
        method="update",
        args=[{
            # 当前选中指标的热力图可视，其他都不可视
            "visible": [i//4 == metric_columns.index(z_param) and i%4 == 3 for i in range(len(metric_columns)*4)] + [False]
        }, {
            "scene": {"visible": False},
            "xaxis": {"visible": True},
            "yaxis": {"visible": True}
        }]
    ))
    
    # 数据表按钮
    view_buttons.append(dict(
        label="数据表",
        method="update",
        args=[{
            # 只显示数据表，其他都不可视
            "visible": [False] * (len(metric_columns)*4) + [True]
        }, {
            "scene": {"visible": False},
            "xaxis": {"visible": False},
            "yaxis": {"visible": False}
        }]
    ))
    
    # 添加指标选择下拉菜单和视图切换按钮
    fig.update_layout(
        updatemenus=[
            # 指标选择下拉菜单
            dict(
                type="dropdown",
                direction="down",
                active=metric_columns.index(z_param),
                x=0.1,
                y=1.15,
                buttons=metric_buttons,
                bgcolor='#F9F9F9',
                bordercolor='#DDDDDD',
                font=dict(size=12),
                pad=dict(t=5, b=5),
                showactive=True,
            ),
            # 视图切换按钮
            dict(
                type="buttons",
                direction="right",
                active=0,
                x=0.5,
                y=1.15,
                buttons=view_buttons,
                bgcolor='#F9F9F9',
                bordercolor='#DDDDDD',
                font=dict(size=12),
                pad=dict(t=5, r=5, b=5, l=5),
            )
        ],
        annotations=[
            dict(text="选择指标:", x=0.01, y=1.15, xref="paper", yref="paper", showarrow=False, font=dict(size=12))
        ]
    )
    
    # 生成所有指标的结果摘要数据，用于动态切换
    summary_data = {}
    for metric, best_point in best_points.items():
        summary_data[metric] = {
            'metric': metric,
            'best_x': float(best_point['x']) if isinstance(best_point['x'], (np.integer, np.floating)) else best_point['x'],
            'best_y': float(best_point['y']) if isinstance(best_point['y'], (np.integer, np.floating)) else best_point['y'],
            'best_z': float(best_point['z']) if isinstance(best_point['z'], (np.integer, np.floating)) else best_point['z'],
            'best_text': best_point['text']
        }
    
    # 创建JSON序列化时的NumPy类型处理函数
    def json_numpy_serializer(obj):
        if isinstance(obj, (np.integer, np.floating, np.bool_)):
            return obj.item()  # 转换为Python原生类型
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        raise TypeError(f'Object of type {type(obj).__name__} is not JSON serializable')
        
    # 将摘要数据转换为JSON字符串，使用自定义序列化器
    summary_json = json.dumps(summary_data, default=json_numpy_serializer)
    
    # 创建初始摘要HTML (将在前端动态更新)
    best_point = best_points[z_param]
    metadata_html = f"""
    <div style="margin-top: 20px; padding: 15px; background-color: #f1f1f1; border-radius: 5px;">
        <h2>参数优化结果摘要</h2>
        <div id="optimization-summary">
            <p id="current-metric">优化指标: {z_param}</p>
            <p id="best-params">最优参数组合: {x_param}={best_point['x']}, {y_param}={best_point['y']}</p>
            <p id="best-value">最优指标值: {best_point['z']:.6f}</p>
            <p id="best-description">{best_point['text']}</p>
        </div>
    </div>
    """
    
    # 生成HTML
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>{title}</title>
        <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
        <style>
            body {{font-family: Arial, sans-serif; margin: 0; padding: 20px;}}
            .container {{max-width: 1200px; margin: 0 auto;}}
            h1 {{color: #333; text-align: center;}}
            .description {{margin-bottom: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 5px;}}
        </style>
    </head>
    <body>
        <div class="container">
            <h1>{title}</h1>
            <div class="description">
                <p>此图表显示了参数优化的结果，展示了不同{x_param}和{y_param}值对{z_param}的影响。颜色用{color_param}表示。</p>
                <p>使用上方的按钮切换不同的可视化方式。您可以在3D表面上拖动、缩放和旋转视图以查看最优区域。</p>
            </div>
            <div id="plotly-div" style="height: 700px;"></div>
            {metadata_html}
        </div>
        <script>
            // 存储所有指标的摘要数据
            var summaryData = {summary_json};
            var plotlyData = {fig.to_json()};
            var currentMetric = '{z_param}';
            var savedCamera = null;  // 用于保存当前相机设置

            // 初始化图表
            Plotly.newPlot('plotly-div', plotlyData.data, plotlyData.layout, {{responsive: true}});

            // 监听图表相机变化，实时保存当前相机位置
            document.getElementById('plotly-div').on('plotly_relayout', function(eventdata) {{
                // 如果有相机信息，则保存
                if (eventdata['scene.camera']) {{
                    savedCamera = eventdata['scene.camera'];
                }}
            }});

            // 监听图表更新事件
            document.getElementById('plotly-div').on('plotly_afterupdate', function(ed) {{
                // 获取当前选中的指标
                let activeIndex = plotlyData.layout.updatemenus[0].active;
                if (activeIndex !== undefined && activeIndex !== null) {{
                    let metricButtons = plotlyData.layout.updatemenus[0].buttons;
                    if (metricButtons && metricButtons.length > activeIndex) {{
                        let metricLabel = metricButtons[activeIndex].label;
                        if (metricLabel && metricLabel !== currentMetric) {{
                            currentMetric = metricLabel;
                            updateSummary(currentMetric);
                        }}
                    }}
                }}
            }});
            
            // 重写点击指标按钮的行为
            var menus = document.getElementsByClassName('updatemenu-item');
            for (var i = 0; i < menus.length; i++) {{
                var items = menus[i].getElementsByTagName('a');
                for (var j = 0; j < items.length; j++) {{
                    // 为指标选择下拉菜单的按钮添加点击事件
                    items[j].addEventListener('click', function(e) {{
                        // 获取选中的指标名称
                        var metricName = this.textContent.trim();
                        if (metricName !== currentMetric) {{
                            // 切换指标时更新摘要
                            currentMetric = metricName;
                            updateSummary(currentMetric);
                            
                            // 快速切换图表显示 并 保持相机视角
                            setTimeout(function() {{
                                if (savedCamera) {{
                                    // 手动更新相机位置而不重置视角
                                    Plotly.relayout('plotly-div', {{'scene.camera': savedCamera}});
                                }}
                            }}, 100);  // 延迟一下确保指标切换完成
                        }}
                    }});
                }}
            }}
            
            // 用于更新摘要信息的函数
            function updateSummary(metric) {{
                if (summaryData[metric]) {{
                    let data = summaryData[metric];
                    document.getElementById('current-metric').textContent = '优化指标: ' + metric;
                    document.getElementById('best-params').textContent = '最优参数组合: {x_param}=' + data.best_x + ', {y_param}=' + data.best_y;
                    document.getElementById('best-value').textContent = '最优指标值: ' + data.best_z.toFixed(6);
                    if (document.getElementById('best-description')) {{
                        document.getElementById('best-description').textContent = data.best_text;
                    }}
                }}
            }}
        </script>
    </body>
    </html>
    """
    
    # 保存HTML文件
    os.makedirs(os.path.dirname(os.path.abspath(filename)), exist_ok=True)
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(html_content)
    
    return os.path.abspath(filename)
